Skip to content

feat(memory): add optional Mem0 backend for skill iteration and reflection tracking - #118

Open
jrauch713-svg wants to merge 2 commits into
microsoft:mainfrom
jrauch713-svg:feat/mem0-memory-integration
Open

feat(memory): add optional Mem0 backend for skill iteration and reflection tracking#118
jrauch713-svg wants to merge 2 commits into
microsoft:mainfrom
jrauch713-svg:feat/mem0-memory-integration

Conversation

@jrauch713-svg

@jrauch713-svg jrauch713-svg commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Adds an optional, opt-in skillopt/memory backend that persists skill iterations and reflection summaries to Mem0, and reads a small amount of relevant history back into the Reflect stage so the memory participates in training rather than only recording it.

Revised to the reviewable minimum requested in review. The two substantive objections — silent enablement and a write-only design — are both addressed.

What changed since the first version

1. Explicit opt-in. Uploading requires train.mem0_enabled: true. A MEM0_API_KEY present in the environment for another application no longer enables anything; a disabled run does not even read the key. All mem0_* resolution lives in skillopt/memory/settings.py.

2. Redaction before any outbound request. skillopt/memory/redaction.py is a single choke point — no payload reaches the client without passing through redact_for_upload. It strips vendor API keys, bearer/basic tokens, JWTs, private keys, key = value secret assignments, the project root, and /home/<user>-style prefixes. Credentials are scrubbed before paths are collapsed, so a key embedded in a home path cannot survive. Relative paths and filenames are deliberately preserved so stored memories stay useful.

The patterns mirror skillopt_sleep/staging.py rather than importing it, since pyproject.toml keeps that package decoupled with zero research dependency.

3. Stable project-specific namespace. Memories are scoped to skillopt:<env>:<sha256-prefix-of-project-root> — stable across runs of one project, distinct across projects, and the raw path is hashed rather than transmitted. Replaces the previous config-name/env user id that could mix unrelated projects.

4. One bounded retrieval call before reflection. hook_pre_reflect fetches relevant history and appends it to the reflection input, which is what turns the integration from external logging into a memory loop. It is assigned to a separate reflect_context variable so the autonomous learning-rate decision and the rewrite prompt continue to see the unaugmented context.

Also included

  • Every call is wall-clock bounded by mem0_timeout_seconds (default 5s), so an unreachable service costs one bounded pause instead of blocking a training step.
  • Malformed patches (None, strings, non-lists) are filtered rather than raised inside a swallowing try.
  • mem0 optional extra declared, deliberately outside all.
  • mem0_* keys mapped through _FLATTEN_MAP under train.. Without this they were silently dropped for structured configs — which is every config in the repo — leaving the feature inert with no error.
  • configs/features/mem0_memory.yaml, a documented opt-in example following the soft_gate.yaml convention.
  • docs/reference/config.md documents every key and exactly what leaves the machine.

Deliberately not included

Per review guidance: batching, async writes, sophisticated ranking, and a broad benchmark study are left as follow-up work.

Test plan

tests/test_mem0_memory.py — 15 cases, no network. Runs under pytest or directly via python tests/test_mem0_memory.py.

  • A bare MEM0_API_KEY does not enable uploads, and no backend is constructed
  • Hooks are no-ops when memory is disabled
  • Explicit opt-in writes to the backend
  • Secrets and home paths are stripped; useful structure preserved
  • Payload cap applied after redaction
  • A credential embedded in a home path is still redacted
  • Namespace is stable per project, distinct across projects, and path-free
  • Explicit mem0_namespace override wins
  • Structured-config keys survive flattening
  • The shipped example config actually enables the feature
  • Retrieved memory reaches the actual reflection prompt, verified end-to-end by capturing the user message handed to the optimizer
  • Retrieval can be disabled independently while writes continue
  • Service failure degrades gracefully; reflection input unchanged
  • Slow service is bounded by mem0_timeout_seconds
  • Malformed patches are filtered, not raised

Full suite: 485 passed, 6 skipped, 0 failed. ruff check clean on all new files. Rebased onto current main.

Note: all tests are mocked — no call has been made against the live Mem0 service.

@Yif-Yang

Copy link
Copy Markdown
Contributor

Thanks for exploring persistent memory for SkillOpt. Memory can be valuable if retrieval measurably improves training, but the current design is not safe to merge yet.

The primary blocker is consent and data handling: merely having a global MEM0_API_KEY causes the trainer to upload up to 4,000 characters of the full skill plus reflection patch summaries to a third-party hosted service. There is no explicit SkillOpt opt-in, redaction, or user-facing disclosure. A key present for another application must not silently enable data export.

Other required changes:

  • Add an explicit config flag/backend selection in addition to the key, defaulting off.
  • Redact secrets and sensitive paths/content before any upload, and document exactly what leaves the machine.
  • Use a stable project-specific namespace; the current env/config-name user ID can mix unrelated projects.
  • Avoid two synchronous network writes per training step; use bounded timeouts plus batching/async behavior.
  • Add the optional dependency/extra and configuration schema/documentation.
  • Validate non-dict or null patches instead of throwing and then swallowing the error.
  • Add mocked tests for disabled, enabled, failure, redaction, namespace, and patch-validation paths.
  • Integrate retrieval into the training loop and provide evidence that it improves quality or utility. At present the trainer only writes; it never calls the retrieval APIs, so this adds privacy/cost/latency without a training benefit.

The CLA check is also still incomplete. Please rework this in the original PR if you would like to continue; that preserves your authorship while allowing a safe review. We can re-review once explicit consent, privacy controls, tests, and a real retrieval benefit are present.

@Yif-Yang Yif-Yang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because the current opt-in and data-handling design can upload private skill and patch content without explicit SkillOpt consent, and the integration lacks tests and a retrieval benefit. Please address the detailed maintainer comment above.

@Yif-Yang

Copy link
Copy Markdown
Contributor

Hi @jrauch713-svg — following up with a concise reminder of what is still required before this can be considered:

  • Explicit opt-in: today a global MEM0_API_KEY silently enables upload. Uploading must be selected explicitly, not inferred from a key that may exist for another application.
  • Redaction of sensitive content before it leaves the process.
  • Stable project-specific namespacing for stored data.
  • A real retrieval benefit: the current integration is write-only and never reads memory back into training.
  • Focused tests covering disabled/enabled behavior, redaction, namespacing, malformed patches, and service failures.
  • Completion of the CLA, which is still pending.

The branch is also conflicting. Do you plan to continue with this direction? If so, please let us know; we are glad to help keep the scope reviewable.

@jrauch713-svg

jrauch713-svg commented Jul 25, 2026 via email

Copy link
Copy Markdown
Author

@Yif-Yang

Copy link
Copy Markdown
Contributor

Not at all — we are not asking you to stop, and you did nothing wrong by exploring this direction. We think a Mem0 integration could be valuable for SkillOpt.

Our review only means that the current implementation needs some iteration before it can be merged. The main concerns are that it can upload skill/reflection content without an explicit SkillOpt opt-in, and that it currently writes memories without retrieving and using them to improve training.

If you would like to continue, we would be happy to help break the work into smaller, clearer steps:

  1. Complete the CLA by replying to the policy bot with the option that correctly applies to you.
  2. Add an explicit, disabled-by-default Mem0 configuration and redact sensitive content before upload.
  3. Add project-specific namespacing and focused tests.
  4. Add retrieval back into the training loop so the integration provides a measurable benefit.

There is no pressure to do everything at once. Please let us know if you would like to continue, and we can help review each step.

@jrauch713-svg

jrauch713-svg commented Jul 25, 2026 via email

Copy link
Copy Markdown
Author

@Yif-Yang

Copy link
Copy Markdown
Contributor

Yes — please continue! We would be very happy to work with you on this, and your instinct to narrow the scope is exactly right. We also appreciate that you identified the write-only design as the central issue.

Our preference would be to keep one small retrieval path in this PR, because that is what turns the integration from external logging into a useful memory loop. It does not need to become a large system. A reviewable minimum would be:

  1. An explicit Mem0 setting that is disabled by default — the presence of MEM0_API_KEY alone must not enable uploads.
  2. Redaction before any outbound request, plus a stable project-specific namespace.
  3. One bounded retrieval call before reflection, with the retrieved context actually included in the reflection input.
  4. Mocked tests showing that disabled mode sends nothing, redaction/namespacing work, retrieved context reaches reflection, and service failures degrade gracefully.

You do not need to solve batching, async writes, sophisticated ranking, or run a large benchmark study in this iteration. A deterministic end-to-end test and a small opt-in smoke example are enough for us to review the correctness of the first useful loop; broader performance evaluation can be follow-up work.

Please take the time you need, complete the CLA using the option that applies to you, and rebase onto current main when convenient. Feel free to push an incremental version or ask questions before everything is finished — we are glad to help, especially for your first contribution here. Thank you for continuing!

@jrauch713-svg

jrauch713-svg commented Jul 25, 2026 via email

Copy link
Copy Markdown
Author

@jrauch713-svg

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

…ction tracking

Wires SkillMemory into ReflACTTrainer via non-breaking hooks: records each
step's skill/score after the evaluation gate and each step's reflection
patches after the accumulation loop. Degrades to a no-op when MEM0_API_KEY
is unset.
…l path

Addresses the review on microsoft#118. The integration was previously write-only and
enabled by the mere presence of MEM0_API_KEY; both are fixed here.

1. Explicit opt-in. Uploading now requires `mem0_enabled: true`. A key present
   in the environment for another application no longer enables anything — a
   disabled run does not even read the key. New `skillopt/memory/settings.py`
   resolves all `mem0_*` config in one place.

2. Redaction before any outbound request. New `skillopt/memory/redaction.py`
   is the single choke point: no payload reaches the client without passing
   through `redact_for_upload`, which strips vendor keys, bearer/basic tokens,
   JWTs, private keys, `key = value` secret assignments, the project root, and
   `/home/<user>`-style prefixes. Credentials are scrubbed before paths are
   collapsed, so a key embedded in a home path cannot survive. Relative paths
   and filenames are preserved so stored memories remain useful. The patterns
   deliberately mirror `skillopt_sleep/staging.py` rather than importing it —
   pyproject keeps that package decoupled with zero research dependency.

3. Stable project-specific namespace. Memories are scoped to
   `skillopt:<env>:<sha256-prefix-of-project-root>`, which is stable across
   runs of one project and distinct across projects. The raw path is hashed,
   never transmitted. Replaces the previous config-name/env user id that could
   mix unrelated projects.

4. One bounded retrieval call before reflection. `hook_pre_reflect` fetches
   relevant history and appends it to the reflection input, turning the
   integration from external logging into a memory loop. It is deliberately
   assigned to a separate `reflect_context` variable so the autonomous
   learning-rate decision and the rewrite prompt continue to see the
   unaugmented context.

Also in this commit:

- Every call is wall-clock bounded by `mem0_timeout_seconds` (default 5s), so
  an unreachable service costs one bounded pause instead of blocking a step.
- Malformed patches (None, strings, non-lists) are filtered rather than raised
  inside a swallowing try.
- The `mem0` optional extra is declared, deliberately outside `all`.
- The `mem0_*` keys are mapped through `_FLATTEN_MAP` under `train.`. Without
  this they were silently dropped for structured configs — which is every
  config in the repo, since they all inherit `_base_/default.yaml` — leaving
  the feature inert with no error.
- `configs/features/mem0_memory.yaml` is a documented opt-in example following
  the `soft_gate.yaml` convention.
- The config reference documents every key and exactly what leaves the machine.

Tests (`tests/test_mem0_memory.py`, 15 cases, no network) cover: a bare
MEM0_API_KEY not enabling uploads, explicit opt-in writing, redaction of
secrets and home paths, cap applied after redaction, namespace stability and
project separation, structured-config keys surviving flattening, the shipped
example config actually enabling the feature, retrieval reaching the actual
reflection prompt (verified end-to-end by capturing the optimizer's user
message), retrieval being independently disableable, graceful degradation on
service failure, timeout bounding, and malformed-patch filtering.

Not included, per review guidance: batching, async writes, ranking, and
benchmark study are left as follow-up work.
Copilot AI review requested due to automatic review settings July 27, 2026 18:30
@jrauch713-svg
jrauch713-svg force-pushed the feat/mem0-memory-integration branch from 64326a5 to 27d6cd9 Compare July 27, 2026 18:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in persistent memory integration for SkillOpt training runs, backing storage/retrieval with Mem0 and wiring it into the ReflACT training loop via hooks that no-op unless explicitly enabled.

Changes:

  • Introduces a new skillopt.memory package (settings resolution, redaction, Mem0 backend, and trainer hooks) to store skill iterations and reflection summaries and optionally retrieve relevant context for reflection.
  • Wires memory hooks into the main trainer loop and extends config flattening plus docs/example config to expose train.mem0_* parameters.
  • Adds an end-to-end mocked test suite validating opt-in safety, redaction, namespacing, retrieval plumbing, graceful failure, time bounds, and malformed patch handling.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_mem0_memory.py Adds comprehensive mocked coverage for opt-in behavior, redaction, retrieval, and failure/timeout handling.
skillopt/memory/trainer_hooks.py Implements maybe_init_mem0 plus pre/post hooks to read/write memory around reflect/evaluate.
skillopt/memory/settings.py Adds strict opt-in settings resolution (never consult env key unless enabled) and namespacing.
skillopt/memory/redaction.py Centralizes outbound redaction for secrets and machine-identifying paths.
skillopt/memory/mem0_backend.py Implements the Mem0 client wrapper with redaction, truncation, bounded calls, and persistence APIs.
skillopt/memory/init.py Exposes the new memory package public API.
skillopt/engine/trainer.py Calls the memory hooks around reflect/evaluate and initializes the backend once per run.
skillopt/config.py Maps structured train.mem0_* config keys into the flattened trainer config.
pyproject.toml Adds a mem0 optional dependency extra (mem0ai).
docs/reference/config.md Documents mem0 memory configuration, safety properties, and what data leaves the machine.
configs/features/mem0_memory.yaml Adds a shipped example config that enables mem0 memory explicitly under train:.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1048 to +1052
# ── Training loop ────────────────────────────────────────────────
t_loop_start = time.time()

memory = maybe_init_mem0(cfg)

Comment on lines +103 to +105
except _FuturesTimeout:
print(f" [mem0] call exceeded {self.settings.timeout_seconds}s — continuing without it")
return None
Comment on lines +245 to +248
text = rec.get("memory") or rec.get("text") or ""
if not isinstance(text, str) or not text.strip():
continue
lines.append(f"- {text.strip()}")
@jrauch713-svg

Copy link
Copy Markdown
Author

Hi @Yif-Yang — pushed. The branch is rebased onto current main and the four items from your reviewable minimum are in.

1. Explicit opt-in. Uploading now requires train.mem0_enabled: true. A MEM0_API_KEY present for another application no longer enables anything — a disabled run doesn't even read the key. All mem0_* resolution is in skillopt/memory/settings.py.

2. Redaction + namespacing. skillopt/memory/redaction.py is a single choke point; no payload reaches the client without passing through it. It strips vendor keys, bearer/basic tokens, JWTs, private keys, key = value secret assignments, the project root, and /home/<user> prefixes. Credentials are scrubbed before paths are collapsed, so a key embedded in a home path can't survive. Relative paths and filenames are preserved so stored memories stay useful.

Namespaces are skillopt:<env>:<sha256-prefix-of-project-root> — stable across runs of one project, distinct across projects, and the raw path is hashed rather than sent.

I mirrored the secret patterns from skillopt_sleep/staging.py rather than importing them, since pyproject.toml keeps that package decoupled with zero research dependency. Happy to change that if you'd rather share the code.

3. One bounded retrieval call before reflection. hook_pre_reflect fetches relevant history and appends it to the reflection input. I deliberately assigned it to a separate reflect_context variable rather than reusing step_buffer_context, because that same variable also feeds the autonomous learning-rate decision and the rewrite prompt — those keep seeing the unaugmented context.

4. Mocked teststests/test_mem0_memory.py, 15 cases, no network. Covers: a bare MEM0_API_KEY not enabling uploads, explicit opt-in writing, redaction of secrets and home paths, the cap applied after redaction, namespace stability and project separation, retrieval reaching the actual reflection prompt (verified end-to-end by capturing the user message handed to the optimizer), retrieval being independently disableable, graceful degradation on service failure, timeout bounding, and malformed-patch filtering.

Also in this push:

  • Every call is wall-clock bounded by mem0_timeout_seconds (default 5s), so an unreachable service costs one bounded pause instead of blocking a step.
  • Malformed patches (None, strings, non-lists) are filtered rather than raised inside a swallowing try.
  • mem0 optional extra declared, deliberately outside all.
  • configs/features/mem0_memory.yaml as a documented opt-in example, following the soft_gate.yaml convention.
  • docs/reference/config.md documents every key plus exactly what leaves the machine.

Two things I hit while rebasing that are worth flagging:

  • The original diff showed ~20 changed lines in trainer.py that weren't real changes — a LF→CRLF conversion of the configure_qwen_chat block. That's also what caused the conflict with the newer configure_cursor_exec block. The actual integration is 12 lines; it's cleaned up now.
  • The mem0_* keys needed mapping into _FLATTEN_MAP under train.. As flat top-level keys they were silently dropped for structured configs — which is every config in the repo — so the feature would have appeared inert with no error. Regression test added for that.

Full suite: 485 passed, 6 skipped, 0 failed. ruff check clean on all new files.

One honest caveat: all tests are mocked — nothing has been run against the live Mem0 service yet. If you'd like a real smoke run before merging, say so and I'll do it.

Left out per your guidance: batching, async writes, ranking, and benchmark evaluation.

Thanks again for the detailed review and for breaking it into steps — it made this much easier to approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants